package jmine.tec.proxy.chain;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Implementation of {@link InvocationChain} that receives an array of
 * {@link ChainElement} and a delegate method and calls the chain and the
 * delegate.
 * 
 * This object is not synchronized and is not serializable.
 * 
 * Instances of this class can be reused after the
 * {@link DelegatingInvocationChain#reset(Method, Object[])} method is called,
 * but shouldnt be shared across multiple invocations at the same time.
 * 
 * @author takeshi
 * 
 * @see InvocationChain
 * 
 */
public class DelegatingInvocationChain implements InvocationChain {
    private final Method method;

    private final Object[] params;

    private int index = 0;

    private final int max;

    private final ChainElement[] el;

    private final Object delegate;

    private final Object proxy;

    /**
     * C'tor
     * 
     * @param delegate
     *            the deleagate object
     * @param el
     *            the ChainElements
     * @param method
     *            the {@link Method}
     * @param proxy
     *            the proxy objecton witch the method was originally invoked
     * @param args
     *            the arguments
     */
    public DelegatingInvocationChain(final Object delegate, final ChainElement[] el,
            final Method method, final Object proxy, final Object[] args) {
        super();
        this.delegate = delegate;
        this.el = el;
        this.method = method;
        this.params = args;
        this.proxy = proxy;
        max = el.length;
    }

    /**
     * @return {@link Object}
     */
    public final Object getBean() {
        return this.delegate;
    }

    /**
     * @return {@link Method}
     */
    public final Method getInvokingMethod() {
        return this.method;
    }

    /**
     * @return {@link Object}
     */
    public final Object[] getParams() {
        return this.params;
    }

    /**
     * 
     * Proceedes the invocation chain, invoking the next
     * {@link ChainElement#aroundInvoke(InvocationChain)} or calling the
     * original method on the delegate if this {@link ChainElement} reached the
     * last element
     * 
     * @return {@link Object} the object to return. If the object is not
     *         compatible with the method signature, a
     *         {@link ClassCastException} will be thrown when the invocation
     *         ends
     * @throws Throwable
     *             if any exception was thrown. Altough it does trap the
     *             {@link InvocationTargetException} so the correct exception is
     *             thrown
     */
    public final Object proceed() throws Throwable {
        if (this.index < max) {
            return this.el[this.index++].aroundInvoke(this);
        }
        return invokeEndOfChain();
    }

    /**
     * Invokes the method on the delegate. Indicates that the end of the chain
     * has been reached. Subclasses overriding this method are encouraged to not
     * let the {@link InvocationTargetException} be thrown, but they do not need
     * to check for type compatibility as {@link ClassCastException} will be
     * eventually thrown or a {@link ChainElement} may fix that.
     * 
     * @return Object to return
     * @throws Throwable
     *             if an exception is thrown
     */
    protected Object invokeEndOfChain() throws Throwable {
        try {
            return this.method.invoke(this.delegate, this.params);
        } catch (IllegalAccessException e) {
            throw new IllegalArgumentException("IllegalAccess accessing method: " + this.method, e);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }

    /**
     * @return the method
     */
    public final Method getMethod() {
        return this.method;
    }

    /**
     * @return the index
     */
    public final int getIndex() {
        return this.index;
    }

    /**
     * @return {@link Object}
     * @see br.com.maps.util.proxy.chain.InvocationChain#getProxyObject()
     */
    public final Object getProxyObject() {
        return this.proxy;
    }
}
